#include "vtkFunctions_import.hpp"
#include "vtkFunctions.hpp"

#include "vtkMeshExport.hpp"
#include "vtkFieldExport_constant.hpp"
#include "vtkFieldExport_source.hpp"
#include "vtkFunctions_export.hpp"

#include "sweSolver.hpp"

namespace hms::vtk
{

void readXML( xml::XMLDocument* doc, const fs::path& path ){
	if ( !fs::is_regular_file(path) ){
		throw FileError(
			"File \"" + path.string() + "\" does not exist!"
		);
	}
	
	// doc->LoadFile( path.c_str() );
	/* LoadFile wants an ascii string (like a const char*), but fs::path::c_str() is const wchar_t* on windows */
	doc->LoadFile( path.string().c_str() );
	if( doc->Error() ){
		doc->PrintError();
		throw ParseError(
			"Failed to parse xml file \"" 
			+ path.string() + "\"!"
		);
	}
}

std::unique_ptr<xml::XMLDocument> readXML( const fs::path& path ){

	auto doc { std::make_unique<xml::XMLDocument>() };
	readXML(doc.get(), path);
	
	return doc;
}

bool createDummy(
	const fs::path& file,
	const std::string_view& explForYes
){
	if ( fs::is_regular_file(file) )
		return false;

	log::info(
		"File \"{1}\"\n"
		"does not exist. Create dummy file?"
		"\n{0}Yes (default): {2}"
		"\n{0}no: Program terminates.",
		indent(),
		bold( file.string() ),
		explForYes
	);
	return getUserChoice();
}

void getBoundaryDoc(
	xml::XMLDocument* doc,
	const fs::path& caseDir,
	bool& dummyCreated,
	bool dryRun
){
	fs::path boundaryPath { getBoundaryPath(caseDir) };

	if ( createDummy( boundaryPath,
		"Creates an empty boundary file"
		"\n\t(patches and boundary conditions must be added manually!).")
	){
		boundaryToXML(doc, {});
		dummyCreated = true;

		if (dryRun) return;

		toFile( doc, boundaryPath );
	}

	readXML( doc, boundaryPath );
}

void getConstantFieldsDoc(
	xml::XMLDocument* doc,
	const fs::path& caseDir,
	bool& dummyCreated,
	bool dryRun
){
	fs::path constPath { getConstantFieldsPath(caseDir) };

	if ( createDummy(constPath, "Creates a default constant fields file.") ){
		writeConstantFields(doc, SWESolver{ UniMesh{}, Time{} } );
		dummyCreated = true;

		if (dryRun) return;

		toFile( doc, constPath );
	}

	readXML(doc, constPath);
}

void getSourceFieldsDoc(
	xml::XMLDocument* doc,
	const fs::path& caseDir,
	bool& dummyCreated,
	bool dryRun
){
	fs::path sourcePath { getSourceFieldsPath(caseDir) };

	if ( createDummy(sourcePath, "Creates a default source fields file.") ){
		SWESolver solver { UniMesh{}, Time{} };

		using SolverType = decltype(solver);
		using SourceFieldName = typename SolverType::SourceFieldName;
		for ( int i{0}; i<SolverType::nSourceFields; ++i ){
			solver.useSourceField( static_cast<SourceFieldName>(i), true );
		}

		for ( auto& sourceVariant : solver.sourceFields() ){
			sourceVariant = SourceField<Uniform, UniMesh>{ UniMesh{}, Array1s{0} };
		}

		writeSourceFields(doc, solver);
		dummyCreated = true;

		if (dryRun) return;

		toFile( doc, sourcePath );
	}
	
	readXML( doc, sourcePath );
}

void getBindingDoc(
	xml::XMLDocument* bindingFile,
	const fs::path& bindingFilePath
){
	readXML( bindingFile, bindingFilePath );
}

void getBindingDoc(
	xml::XMLDocument* bindingFile,
	const fs::path& caseDir, const std::string& caseName
){
	getBindingDoc( bindingFile, getBindingFilePath(caseDir, caseName) );
}

std::unique_ptr<xml::XMLDocument> getBindingDoc(
	const fs::path& caseDir, const std::string& caseName
){
	auto doc { std::make_unique<xml::XMLDocument>() };
	getBindingDoc( doc.get(), caseDir, caseName );
	return doc;
}

bool tryMeshExtensions( fs::path& meshPath ){
	std::string oldExtension { meshPath.extension().string() };
	auto isFile = [&]( const auto& ext ) -> bool {
		return fs::is_regular_file(meshPath.replace_extension(ext));
	};
	if ( isFile(uniExt) )
		return true;
	else if ( isFile(rectExt) )
		return true;
	else if ( isFile(structExt) )
		return true;
	else if ( isFile(unstrExt) )
		return true;
	
	meshPath.replace_extension(oldExtension);
	return false;
}

std::vector<FunctionParserWrapper> readFunctions(
	const xml::XMLElement* parent,
	Index nComponents,
	bool optimise
){
	std::vector<FunctionParserWrapper> fparsers;
	fparsers.reserve(nComponents);
	for ( int i{0}; i<nComponents; ++i ){
		fparsers.emplace_back( readFunction(parent, i, optimise) );
	}
	return fparsers;
}


FunctionParserWrapper readFunction(
	const xml::XMLElement* parent,
	Index component,
	bool optimise
){
	const xml::XMLElement* functionElem {
		getChildNodeByAttributeValue( parent, "function", "component", component )
	};
	try {
		FunctionParserWrapper fp { functionElem->GetText(), optimise };
		return fp;
	} catch( std::exception& e ){
		throw std::runtime_error( fmt::format(
			"{}\n{}",
			e.what(),
			nodeString(functionElem)
		) );
	}
}


namespace internal
{

template<typename MeshType>
TimeSeries readTimeSeries(
	const xml::XMLElement* parent,
	Index nComponents,
	Index nCols,
	std::experimental::observer_ptr<const MeshBase<MeshType>> mesh,
	Uniformity isUniform
){
	auto nTimeSeries { getAttributeValue<size_t>(parent, "NumberOfTimeValues") };
	bool domainOnly { mesh };
	if ( domainOnly )
		nCols = nFieldCols(*mesh);

	TimeSeries timeSeries;
	timeSeries.reserve(nTimeSeries);

	const xml::XMLElement* valElem { getChildNodeByName(parent, "Values") };

	for ( size_t i{0}; i<nTimeSeries; ++i ){
		if ( !valElem )
			throw ParseError(
				"Could not read time series #" + std::to_string(i)
				+ "\n" + nodeString(parent)
			);

		ArrayXXs vals;
		/* if a mesh pointer was passed, and non-uniform values are requested,
		 * the values must be resized to fit the field value standard. If no
		 * pointer was passed, the number of columns passed in are used instead,
		 * e.g. for setting ghost values */
		if (mesh){
			resizeFieldValues(vals, nComponents, *mesh, isUniform);
			textToFieldValues(valElem, vals, *mesh, isUniform);
		} else {
			vals.resize( nComponents, nCols );
			textToMatrix( valElem, vals );
		}
		timeSeries.emplace_back(
			getAttributeValue<scalar>(valElem, "Time"), std::move(vals)
		);
		valElem = valElem->NextSiblingElement("Values");
	}
	std::sort( timeSeries.begin(), timeSeries.end(),
		[]( const auto& pair1, const auto& pair2 ){
			return pair1.first < pair2.first;
		}
	);
	return timeSeries;
}

} // namespace internal


TimeSeries readTimeSeries(
	const xml::XMLElement* parent,
	Index nComponents,
	Index nCols
){
	/* any type would work, but using an actual mesh type means one less 
	 * template instantiation */
	return internal::readTimeSeries<UniMesh>(
		parent, nComponents, nCols, nullptr, Uniform
	);
}

template<typename MeshType>
TimeSeries readTimeSeries(
	const xml::XMLElement* parent,
	Index nComponents,
	const MeshBase<MeshType>& mesh,
	Uniformity isUniform
){
	/* the third arg is ignored when a mesh is passed */
	return internal::readTimeSeries<MeshType>(
		parent, nComponents, 1,
		std::experimental::make_observer( &mesh ),
		isUniform
	);
}



const xml::XMLElement* lastWrittenElement( const xml::XMLDocument* doc ){
	const xml::XMLElement* lastChild {
		getChildNodeByName( getVtkNode(doc), "Collection" )
			->LastChildElement("DataSet")
	};
	if ( lastChild )
		return lastChild;
	else
		throw ParseError(
			"Could not find xml node holding last written time step "
			"in binding file!"
		);
}

scalar lastWrittenTime( const xml::XMLElement* dataSet ){
	return getAttributeValue<scalar>( dataSet, "timestep" );
}

scalar lastWrittenTime( const fs::path& caseDir, const std::string& caseName ){
	xml::XMLDocument bindingFile;
	getBindingDoc( &bindingFile, caseDir, caseName );

	return lastWrittenTime( lastWrittenElement(&bindingFile) );
}

/* only returns the file name, not the whole path to it */
std::string lastWrittenFilename( const xml::XMLElement* dataSet ){
	const char* fileName {
		getAttributeValue<const char*>(dataSet, "file" )
	};
	if (fileName)
		return { fileName };
	else // this should already be caught by getAttributeValue
		throw ParseError(
			"Could not find file name of last written time step "
			"in binding file!"
		);
}

/* given the node pointer and case directory,
 * this provides the full path to the last written file.
 * Use as input to mesh and domain field import. */
fs::path lastWrittenFile(
	const xml::XMLElement* dataSet, const fs::path& caseDir
){
	return { caseDir / lastWrittenFilename(dataSet) };
}

fs::path lastWrittenFile(
	const fs::path& caseDir,
	const std::string& caseName
){
	return { caseDir / lastWrittenFilename( lastWrittenElement(
		getBindingDoc(caseDir, caseName).get()
	) ) };
}


std::pair<fs::path, scalar> lastWrittenFileAndTime(
	const fs::path& caseDir,
	const std::string& caseName
){
	std::unique_ptr<xml::XMLDocument> doc { getBindingDoc(caseDir, caseName) };
	const xml::XMLElement* lastElem { lastWrittenElement( doc.get() ) };
	scalar lastTime { lastWrittenTime(lastElem) };
	fs::path vtkFilePath { lastWrittenFile(lastElem, caseDir) };
	if ( !fs::is_regular_file(vtkFilePath) )
		throw FileError(
			"Could not find last written file \""
			+ vtkFilePath.string() + "\"!"
		);
	return {
		vtkFilePath,
		lastTime
	};
}



#define INSTANTIATE_FOR_ALL_MESHTYPES(TYPE) \
template TimeSeries readTimeSeries<TYPE>( \
	const xml::XMLElement*, Index, const MeshBase<TYPE>&, Uniformity );
#include "meshInstantiator.hpp"


} // namespace hms::vtk
